Migrate template widget configuration to Compose and Material3 - #7288
Migrate template widget configuration to Compose and Material3#7288su7ri wants to merge 14 commits into
Conversation
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
🟡 Not ready to approve
The ViewModel can leave a stale “Rendered” preview while a new render is in-flight, allowing users to save an unvalidated template/server selection before rendering completes.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Migrates the Template widget configuration flow from a legacy XML/ViewBinding Activity to a Compose + Material3 screen driven by an assisted Hilt ViewModel, aligning the widget configuration architecture with the recent entity widget migration pattern.
Changes:
- Replaced the legacy
TemplateWidgetConfigureActivityUI with a Compose screen (TemplateWidgetConfigureScreen) underHATheme. - Introduced
TemplateWidgetConfigureViewModel+ immutableTemplateWidgetConfigureState(including aTemplatePreviewsealed type) to own restore/render/save logic viaStateFlow. - Added focused unit tests for restore/render/save behavior; removed the obsolete XML layout.
File summaries
| File | Description |
|---|---|
| app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt | Converts the Activity into a thin Compose wrapper that delegates logic to the ViewModel. |
| app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureScreen.kt | New Compose UI for configuring the widget, including HTML preview rendering. |
| app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureState.kt | Adds immutable UI state and a sealed preview model for rendered/empty/error states. |
| app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModel.kt | New assisted Hilt ViewModel handling restore/render/save and widget pin/update flows. |
| app/src/test/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModelTest.kt | Adds unit tests for state restoration, rendering, and persistence behavior. |
| app/src/main/res/layout/widget_template_configure.xml | Removes the legacy XML configuration layout. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
jpelgrom
left a comment
There was a problem hiding this comment.
Thanks for picking this up!
Could you add a screenshot test as well? You already have a Compose preview so it shouldn't be too difficult :)
The live template preview still renders basic HTML from the server response; the HTML→AnnotatedString conversion follows the same approach already used on the Wear OS template tile screen (SettingsWearTemplateTile.kt), duplicated here rather than extracted to keep this PR focused on the widget migration. Happy to extract it to a shared util in a follow-up if maintainers prefer.
As it is only ~30 lines you can extract it into a util file in this PR instead of duplicating code.
…iew card, multi-line template field
Fixes an Android Lint (Slack compose-lints) finding: composables should only emit content from one source at their top level.
jpelgrom
left a comment
There was a problem hiding this comment.
Almost there!
Compared to the old activity text alignment via HTML no longer shows in the preview but as it's quite an advanced feature and still working in the widget it's fine as it is right now so we can avoid complicating it even more. If more users show up with complicated HTML examples we can always fallback to a classic view which takes the HTML.
| Before | After |
|---|---|
![]() |
(theptag is adding padding at the bottom) |
| modifier = Modifier.size(HAPreviewIconSize), | ||
| ) | ||
| Text( | ||
| text = stringResource(commonR.string.template_preview_label), |
There was a problem hiding this comment.
I think this would look slightly better if it's the size of a title in a dropdown, what do you think?
| modifier = Modifier.size(HAPreviewIconSize), | |
| ) | |
| Text( | |
| text = stringResource(commonR.string.template_preview_label), | |
| modifier = Modifier.size(HASize.M), | |
| ) | |
| Text( | |
| text = stringResource(commonR.string.template_preview_label), | |
| style = HATextStyle.BodyMedium, |
Don't forget to update screenshot tests if you accept.
If you don't agree update the size to use one of our shared constants:
| modifier = Modifier.size(HAPreviewIconSize), | |
| ) | |
| Text( | |
| text = stringResource(commonR.string.template_preview_label), | |
| modifier = Modifier.size(HASize.L), | |
| ) | |
| Text( | |
| text = stringResource(commonR.string.template_preview_label), |
| import io.homeassistant.companion.android.common.compose.composable.HATextField | ||
| import io.homeassistant.companion.android.common.compose.composable.HATopBar | ||
| import io.homeassistant.companion.android.common.compose.theme.HADimens | ||
| import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview |
There was a problem hiding this comment.
If you agree with https://github.com/home-assistant/android/pull/7288/changes#r3714512883
| import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview | |
| import io.homeassistant.companion.android.common.compose.theme.HATextStyle | |
| import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview |
|
I forgot we should have compose tests that verify that the callbacks are properly triggered and also the content is adjusting properly based on the UI State like showing the server selector, the text size error, the action button has the right text and it is disabled/enabled on the right conditions. |
Addresses TimoPtr's review comment requesting coverage for the shared HTML-to-AnnotatedString conversion used by rendered templates.
- Drop the enabled=items.isNotEmpty() guard on the server selector, matching jpelgrom's resolution that show/enable should share the view state's own showServerSelector condition. - Turn TemplateSection/AppearanceSection into ColumnScope receivers instead of wrapping their content in an extra Column. - Move the text size field into AppearanceSection. - Use HASize.M for the preview icon and HATextStyle.BodyMedium for the preview label/text. - Clarify HATextField's singleLine/maxLines doc to mention the default relationship between the two. - Add ViewModel test coverage verifying renderTemplate is/isn't invoked on blank vs. non-blank templates, and that a superseded render isn't left running; move BLACK_HEX out of a companion object. - Add a Compose UI test for TemplateWidgetConfigureContent verifying callbacks fire and content adjusts to state (navigation, server selection, template/text-size input, background/text-color pickers, preview states, action enablement). Reference screenshots under screenshotTestFullDebug still reflect the pre-refactor layout/styling and need regenerating in an environment with access to the Google Maven repository, which this sandbox lacks.
Addresses jpelgrom's comment on TEMPLATE_FIELD_MIN_LINES and TimoPtr's comment on BLACK_HEX: both belong before the first function/class rather than at the bottom of the file.
… size - Memoize the HTML parse in TemplatePreviewCard on the rendered text, since it doesn't need to re-run on every recomposition. Only the parsing itself is remembered, not the whole toAnnotatedString() call, since remember's calculation lambda disallows the composable stringResource() calls used by the other TemplatePreview branches. - isActionEnabled now also requires a valid text size, so an emptied size field can't silently fall back to the default on save.
- DEFAULT_TEXT_SIZE is only used within its own file, so narrow it from internal to private. - Move restoreConfiguration() and renderTemplate() to the bottom of TemplateWidgetConfigureViewModel, after the public/internal API, per TimoPtr's comment (private functions go last even when used from the constructor/init block).
- getPendingDaoEntity() is only used within the ViewModel, so narrow it to private and move it next to the other private members at the bottom of the file. - Match HATextField's minLines doc wording to maxLines'. - Add coVerify assertions that clearing/blanking the template doesn't trigger an extra render call. - Add a single-server screenshot test demonstrating the server selector stays hidden, and use a more representative rendered template (color span, bold, line break) for the existing screenshot tests instead of a bare number.
|
Thanks for taking the time to contribute. This contribution looks like it doesn't follow our AI policy, so I'm flagging it here. If I've got this wrong, please let me know. We won't close it this time because @jpelgrom and myself already reviewed it, but please keep this in consideration for future contributions to this PR or future PRs. |
| } | ||
| } | ||
|
|
||
| private companion object { |
There was a problem hiding this comment.
This doesn't need to be in a companion object
Thanks for flagging this. I'll keep using AI as a tool since I'm still learning this codebase, but I'll make sure to actually understand and review each change before it goes up, and commit things myself instead of letting the tool push directly. |


Summary
Migrates
TemplateWidgetConfigureActivityfrom a legacy XML/View Bindinglayout to Jetpack Compose with Material 3, following the same pattern
established for the entity widget in #7007 (referenced from #6307 as the
example to follow for this series of widget migrations).
TemplateWidgetConfigureActivity.ktis now a thin wrapper (~60 lines,down from ~250) around:
TemplateWidgetConfigureState: immutable UI state, including aTemplatePreviewsealed type (Empty/Rendered/Error) replacing the oldsingle TextView that mixed rendered output, error messages, and the
"blank" placeholder.
TemplateWidgetConfigureViewModel: the restore/save/render logic,moved out of the Activity into a Hilt ViewModel with
StateFlow.TemplateWidgetConfigureScreen: the Compose UI, reusing the sharedWidgetBackgroundTypeDropdown/WidgetTextColorDropdowncomposables.The live template preview still renders basic HTML from the server
response; the HTML→AnnotatedString conversion follows the same approach
already used on the Wear OS template tile screen
(
SettingsWearTemplateTile.kt), duplicated here rather than extractedto keep this PR focused on the widget migration. Happy to extract it to
a shared util in a follow-up if maintainers prefer.
Closes #6304
Checklist
Select exactly one option that describes AI usage in this contribution:
Screenshots
Any other notes
Part of the ongoing effort to migrate widget configuration screens to
Compose/Material 3 (see #6303, #6305, #6306, #6308, and others).